package org.xx.armory.xml;

import org.xx.armory.commons.Validators;

import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * 能够自动缩进的XML流书写器。
 *
 * @author Haart
 */
public final class IndentedXMLStreamWriter
        implements XMLStreamWriter {
    private static final int DEFAULT_INDENT_SIZE = 2;
    private static final String NEWLINE = "\n";

    private final XMLStreamWriter writer;
    private final String indentText;
    private final Deque<Boolean> hasChildren;
    private int indent;
    private boolean needNewline;
    private boolean isEmptyElement;

    /**
     * 构造{@link IndentedXMLStreamWriter}类型的实例。
     *
     * @param writer
     *         内部书写器。
     * @throws NullPointerException
     *         如果参数{@code writer}是{@code null}。
     */
    public IndentedXMLStreamWriter(XMLStreamWriter writer) {
        this(writer, DEFAULT_INDENT_SIZE);
    }

    /**
     * 构造{@link IndentedXMLStreamWriter}类型的实例。
     *
     * @param writer
     *         内部书写器。
     * @param indentSize
     *         每级缩进的空格数。
     * @throws NullPointerException
     *         如果参数{@code writer}是{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code indentSize}小于{@code 0}。
     */
    public IndentedXMLStreamWriter(
            XMLStreamWriter writer,
            int indentSize
    ) {
        Validators.notNull(writer, "writer");
        Validators.greaterThanOrEqual(indentSize, "indentSize", 0);

        this.writer = writer;

        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < indentSize; ++i) {
            sb.append(" ");
        }
        this.indentText = sb.toString();
        this.indent = 0;
        this.needNewline = false;
        this.hasChildren = new ArrayDeque<>();
    }

    /**
     * 执行输出非文本内容之前的准备工作, 如果需要换行则输出新行符, 然后按照缩进层级输出缩进的空白。
     *
     * @throws XMLStreamException
     *         如果调用内部书写器输出字符时出错。
     */
    private void writeIndent()
            throws XMLStreamException {
        if (this.needNewline) {
            this.writer.writeCharacters(NEWLINE);
        }

        if (this.indent <= 0) {
            return;
        }

        for (int i = 0; i < this.indent; ++i) {
            this.writer.writeCharacters(this.indentText);
        }
    }

    /**
     * 执行输出文本内容之前的准备工作, 如果需要换行则输出新行符。
     *
     * @throws XMLStreamException
     *         如果调用内部书写器输出字符时出错。
     */
    private void writeNoIndent()
            throws XMLStreamException {
        if (this.needNewline) {
            this.writer.writeCharacters(NEWLINE);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartElement(String localName)
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartElement(localName);
        this.needNewline = true;
        this.isEmptyElement = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
        this.hasChildren.push(false);

        ++this.indent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartElement(
            String namespaceURI,
            String localName
    )
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartElement(namespaceURI, localName);
        this.needNewline = true;
        this.isEmptyElement = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
        this.hasChildren.push(false);

        ++this.indent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartElement(
            String prefix,
            String localName,
            String namespaceURI
    )
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartElement(prefix, localName, namespaceURI);
        this.needNewline = true;
        this.isEmptyElement = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
        this.hasChildren.push(false);

        ++this.indent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEmptyElement(
            String namespaceURI,
            String localName
    )
            throws XMLStreamException {
        writeIndent();

        this.writer.writeEmptyElement(namespaceURI, localName);
        this.needNewline = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEmptyElement(
            String prefix,
            String localName,
            String namespaceURI
    )
            throws XMLStreamException {
        writeIndent();

        this.writer.writeEmptyElement(prefix, localName, namespaceURI);
        this.needNewline = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEmptyElement(String localName)
            throws XMLStreamException {
        writeIndent();

        this.writer.writeEmptyElement(localName);
        this.needNewline = true;
        this.hasChildren.pop();
        this.hasChildren.push(true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEndElement()
            throws XMLStreamException {
        --this.indent;

        if (!this.isEmptyElement || this.hasChildren.pop()) {
            writeIndent();
        }

        this.writer.writeEndElement();
        this.needNewline = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEndDocument()
            throws XMLStreamException {
        writeIndent();

        this.writer.writeEndDocument();
        this.hasChildren.pop();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close()
            throws XMLStreamException {
        if (this.writer != null) {
            this.writer.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void flush()
            throws XMLStreamException {
        if (this.writer != null) {
            this.writer.flush();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeAttribute(
            String localName,
            String value
    )
            throws XMLStreamException {
        this.writer.writeAttribute(localName, value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeAttribute(
            String prefix,
            String namespaceURI,
            String localName,
            String value
    )
            throws XMLStreamException {
        this.writer.writeAttribute(prefix, namespaceURI, localName, value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeAttribute(
            String namespaceURI,
            String localName,
            String value
    )
            throws XMLStreamException {
        this.writer.writeAttribute(namespaceURI, localName, value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeNamespace(
            String prefix,
            String namespaceURI
    )
            throws XMLStreamException {
        this.writer.writeNamespace(prefix, namespaceURI);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeDefaultNamespace(String namespaceURI)
            throws XMLStreamException {
        this.writer.writeDefaultNamespace(namespaceURI);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeComment(String data)
            throws XMLStreamException {
        writeIndent();

        this.writer.writeComment(data);
        this.needNewline = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeProcessingInstruction(String target)
            throws XMLStreamException {
        this.writer.writeProcessingInstruction(target);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeProcessingInstruction(
            String target,
            String data
    )
            throws XMLStreamException {
        this.writer.writeProcessingInstruction(target, data);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeCData(String data)
            throws XMLStreamException {
        if (data == null || data.length() == 0) {
            return;
        }

        writeIndent();

        this.writer.writeCData(data);
        this.needNewline = true;
        this.isEmptyElement = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeDTD(String dtd)
            throws XMLStreamException {
        writeIndent();

        this.writer.writeDTD(dtd);
        this.needNewline = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeEntityRef(String name)
            throws XMLStreamException {
        writeNoIndent();

        this.writer.writeEntityRef(name);
        this.needNewline = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartDocument()
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartDocument();
        this.needNewline = true;
        this.hasChildren.push(false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartDocument(String version)
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartDocument(version);
        this.needNewline = true;
        this.hasChildren.push(false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeStartDocument(
            String encoding,
            String version
    )
            throws XMLStreamException {
        writeIndent();

        this.writer.writeStartDocument(encoding, version);
        this.needNewline = true;
        this.hasChildren.push(false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeCharacters(String text)
            throws XMLStreamException {
        if (text == null || text.length() == 0) {
            return;
        }

        writeNoIndent();

        this.writer.writeCharacters(text);
        this.needNewline = false;
        this.isEmptyElement = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void writeCharacters(
            char[] text,
            int start,
            int len
    )
            throws XMLStreamException {
        if (text == null || text.length == 0 || len == 0) {
            return;
        }

        writeNoIndent();

        this.writer.writeCharacters(text, start, len);
        this.needNewline = false;
        this.isEmptyElement = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPrefix(String uri)
            throws XMLStreamException {
        return this.writer.getPrefix(uri);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setPrefix(
            String prefix,
            String uri
    )
            throws XMLStreamException {
        this.writer.setPrefix(prefix, uri);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDefaultNamespace(String uri)
            throws XMLStreamException {
        this.writer.setDefaultNamespace(uri);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public NamespaceContext getNamespaceContext() {
        return this.writer.getNamespaceContext();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getProperty(String name)
            throws IllegalArgumentException {
        return this.writer.getProperty(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setNamespaceContext(NamespaceContext context)
            throws XMLStreamException {
        this.writer.setNamespaceContext(context);
    }


}
